package de.tum.in.msspp.graph;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.ListIterator;

import android.content.Context;
import android.util.Log;

public class Graph {

	String name;

	private ArrayList<Node> nodes;
	private ArrayList<Edge> edges;

	/**
	 * Creates a new Graph
	 * @param name - name of the graph
	 */
	public Graph(String name) 
	{
		this.name = name;
		this.nodes = new ArrayList<Node>();
		this.edges = new ArrayList<Edge>();
	}
	
	/**
	 * Creates a new Graph from the given xml
	 * @param name of the graph
	 * @param xml - xml version of the graph
	 */
	public Graph(String name, String xml) 
	{
		this.name = name;
		this.nodes = new ArrayList<Node>();
		this.edges = new ArrayList<Edge>();
		
		//Parse Graph
		ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes());
		GraphXMLParser parser = new GraphXMLParser();
		Graph tmpGraph = parser.xmlToGraph(is);

		// get saved values
		if(tmpGraph != null)
		{			
			this.nodes = tmpGraph.nodes;
			this.edges = tmpGraph.edges;
		}
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Next Node id
	 * @return next unique free id
	 */
	public int nextNodeId() 
	{
		return this.nodes.size()+1;
	}

	/**
	 * Next Edge id
	 * @return next unique free id
	 */
	public int nextEdgeId() 
	{
		return this.edges.size()+1;
	}

	/**
	 * Adds a new node to the graph
	 * @param node - node that should be added
	 * @throws GraphException - Duplicate id
	 */
	public void addNode(Node node) throws GraphException 
	{
		// Check for duplicate
		for (Node cNode : this.nodes)
			if (cNode.getId() == node.getId())
				throw new GraphException("Duplicate Node Id");

		nodes.add(node);
	}

	/**
	 * Adds a new edge to the graph
	 * @param edge - edge taht should be added
	 * @throws GraphException - Duplicate Edge Id
	 */
	public void addEdge(Edge edge) throws GraphException {
		// Check for duplicate
		for (Edge cEdge : this.edges)
			if (cEdge.getId() == edge.getId())
				throw new GraphException("Duplicate Edge Id");

		edges.add(edge);
	}

	/**
	 * 
	 * @return Iterator over all edges
	 */
	public ListIterator<Edge> getEdgeIterator() 
	{
		return this.edges.listIterator();
	}

	/**
	 * 
	 * @return Iterator over all nodes
	 */
	public ListIterator<Node> getNodeIterator() 
	{
		return this.nodes.listIterator();
	}

	/**
	 * Get Egde by id
	 * @param id - id taht identifies the edge
	 * @return Edge with specified id
	 */
	public Edge edgeWithId(int id) 
	{
		for (Edge edge : this.edges)
			if (edge.getId() == id)
				return edge;

		return null;
	}

	/**
	 * Returns a bidirectional Edge between booth nodes
	 * @param from one Node of the Edge
	 * @param to second Node of the Edge
	 * @return Edge between both Nodes
	 */
	public Edge edgeBetweenNodes(Node from, Node to) {
		for (Edge edge : this.edges) 
		{
			//from could be nodeA OR nodeB...
			if ((edge.nodeA == from && edge.nodeB == to) 
				|| (edge.nodeB == from && edge.nodeA == to))
				
				//return existing edge
				return edge;
		}

		//Return new edge
		Edge newEdge = new Edge(nextEdgeId(), from, to, 0, from.getName() + "," + to.getName());
		this.edges.add(newEdge);

		return newEdge;
	}

	/**
	 * Returns the node with the given id
	 * @param id of the node
	 * @return node
	 */
	public Node nodeWithId(int id) 
	{
		for (Node node : this.nodes)
			if (node.getId() == id)
				return node;

		return null;
	}

	/**
	 * Returns the node with the given name
	 * @param name of the node
	 * @return node
	 */
	public Node nodeWithName(String name) 
	{
		for (Node node : this.nodes) {
			if (node.getName().equals(name))
				return node;
		}

		return null;
	}

	/**
	 * Merges the given graph into this graph-object
	 * @param graph taht should be inserted
	 * @throws GraphException
	 */
	public void merge(Graph graph) throws GraphException 
	{
		HashMap<Node, Node> nodeMapping = new HashMap<Node, Node>();

		// Handle Nodes of the new graph
		for (Node nNode : graph.nodes) 
		{
			boolean node_exists = false;
			for (Node oNode : this.nodes) 
			{
				//Found a Matching node in both graphs
				if (nNode.getName().equals(oNode.getName())) 
				{
					// Node already found
					node_exists = true;
					//The new node should be mapped to the original node in this graph
					nodeMapping.put(nNode, oNode);
					
					Log.i("Graph",
							nNode.getIdentifier() + " and "
									+ oNode.getIdentifier() + " called "
									+ nNode.getName());
				}
			}

			if (!node_exists) 
			{
				//The new node doesn't exist here
				Node newNode = new Node(nextNodeId(), nNode.name,
						nNode.identifier, nNode.thumbnail);
				//Add the new Node to this graph, with a new id
				this.nodes.add(newNode);
				//Map the new node to the newNode with a new id
				nodeMapping.put(nNode, newNode);
			}
		}

		// Merge Edges
		for (Edge edge : graph.edges) 
		{
			//Get new Mapped Node
			Node newNodeA = nodeMapping.get(edge.nodeA);
			Node newNodeB = nodeMapping.get(edge.nodeB);

			//Create new Edge between mapped nodes
			Edge newEdge = new Edge(this.nextEdgeId(), newNodeA, newNodeB,
					edge.weight, edge.tags);

			// Check if edge exist in this graph
			boolean edge_exists = false;
			for (Edge oEdge : this.edges) 
			{
				if ((oEdge.nodeA == newEdge.nodeA && oEdge.nodeB == newEdge.nodeB)
				|| (oEdge.nodeA == newEdge.nodeB && oEdge.nodeB == newEdge.nodeA)) 
				{
					// Edge found in current graph
					edge_exists = true;
					
					//update weight
					oEdge.setWeight(oEdge.getWeight() + newEdge.getWeight());
				}
			}
			
			//Its a new Edge
			if (!edge_exists)
				this.edges.add(newEdge);
		}
	}

	/**
	 * Saves the graph to disk
	 * @param context
	 */
	public void save(Context context) 
	{
		try 
		{
			FileOutputStream fos = context.openFileOutput(name,
					Context.MODE_PRIVATE);

			//parse Graph
			GraphXMLParser parser = new GraphXMLParser();
			String xmlGraph = parser.graphToXml(this);

			//save Graph
			fos.write(xmlGraph.getBytes());
			fos.close();
		} 
		catch (FileNotFoundException e) 
		{
			Log.i("Graph", "Unable to write file");
			e.printStackTrace();
		}
		catch (IOException e) 
		{
			Log.i("Graph", "Exception on saving Graph");
			e.printStackTrace();
		}
	}

	/**
	 * Loads the graph from disk
	 * @param context
	 */
	public void load(Context context) 
	{
		try 
		{
			FileInputStream fis = context.openFileInput(name);
		
			//Parse Graph
			GraphXMLParser parser = new GraphXMLParser();
			Graph tmpGraph = parser.xmlToGraph(fis);
			
			fis.close();

			// get saved values
			if(tmpGraph != null)
			{			
				this.nodes = tmpGraph.nodes;
				this.edges = tmpGraph.edges;
			}

		} 
		catch (FileNotFoundException e) 
		{
			Log.i("Graph", "New Graph!");
		} 
		catch (IOException e) 
		{
			Log.i("Graph", "Exception on loading Graph!");
			e.printStackTrace();
		}
	}
	
	/**
	 * Parses the Graph object to xml
	 * @return xml representation of the graph
	 */
	public String toXML()
	{
		//parse Graph
		GraphXMLParser parser = new GraphXMLParser();
		return parser.graphToXml(this);
	}
}
